package puntaelena.facilitylocation.hillclimbing.solver.agents;

import static puntaelena.facilitylocation.hillclimbing.solver.agents.FcltCityNegotiateComputator.*;

import java.util.*;

import puntaelena.facilitylocation.hillclimbing.solver.services.*;
import puntaelena.graphsupport.FacilitiesGraph;
import puntaelena.graphsupport.GraphSupportFront;
import laboratoryQ.computation.BaseUtils;
import jfuzzyprincepless.negotiation.INegotiationContext;
import jfuzzyprincepless.algorithm.structure.services.IWhileService;
import jfuzzyprincepless.agents.BaseNegotiationAgent;

public class FacilityCoalition
	extends BaseNegotiationAgent
{
	public static final FacilityCoalition EMPTY;
	static {
		IFcltLocDataService flServ =
			new DefaultFcltLocDataService();
		flServ.setFacilityLocationData(
			new FacilitiesGraph(0, 0),
			new boolean[0],
			new int[0]
		);
		EMPTY = new FacilityCoalition(null, flServ);
		EMPTY.members = Collections.emptySet();
	}

	// ### ### ### ### ### ### ### ### ### ### ### ### ### 

	private CollaboratingFacilityAgent ownerAgent;
	private Set<CollaboratingFacilityAgent> members =
		new LinkedHashSet<CollaboratingFacilityAgent>();
	private double[] enemyRate;

	private boolean useFcltRepaymentHeuristic = false;
	private int fcltRepayCoalLimit = 2;
	private double eps = 1e-5;
	
	private int solutionVersion = NOT_INIT_VERSION;
	/**
	 * internal
	 */
	private int negotiationegVersion = NOT_INIT_VERSION;
	/**
	 * external
	 */
	private int coalitionVersion = INIT_VERSION;

	public FacilityCoalition(
		CollaboratingFacilityAgent ownerAgent,
		IFcltLocDataService flServ
	) {
		this.ownerAgent = ownerAgent;
		members.add(ownerAgent);
		int facilitiesCount =
			flServ.getFgraph().getFacilitiesCount();
		enemyRate = new double[facilitiesCount];
	}

	public boolean isOwner(CollaboratingFacilityAgent agent) {
		return ownerAgent == agent;
	}

	public Set<CollaboratingFacilityAgent> getMembers() {
		return members;
	}

	public double[] getEnemyRate() {
		return enemyRate;
	}

	// ### ### ### ### ### ### ### ### ### ### ### ### ###

	public boolean coalitionNegotiate(INegotiationContext context) {
		IFcltLocDataService flServ =
			context.getService(IFcltLocDataService.class);
		IHillClimbingAgentRegistry hcagReg =
			context.getService(IHillClimbingAgentRegistry.class);
		IWhileService whileService =
			context.getService(IWhileService.class);

		FacilitiesGraph fgraph = flServ.getFgraph();
		int[] witnesses = flServ.getWitnesses();

		int solutionVersion = flServ.getSolutionVersion();
		if (
			this.solutionVersion == solutionVersion &&
			negotiationegVersion == coalitionVersion
		) {
			assert _ac_sameRateANDProfitNull(context);
			return whileService.isAgentAlive(this); // TODO: is not a real agent -- fix it
		}
		syncNextVersion(flServ, context.getService(ICoalitionsRegistry.class));

		List<Integer> enemies = new ArrayList<Integer>();

		context.getService(ICoalitionsListener.class)
			.onComputeTryApplyProfitBeg();
		double profit = computeTryApplyProfit(
			flServ,
			hcagReg,
			enemies,
			context.getService(ICoalitionsListener.class),
			enemyRate
		);
		context.getService(ICoalitionsListener.class)
			.onComputeTryApplyProfitEnd();

		if (profit > 0) {
			flServ.increaseSolutionVersion();
			double costPre = _ac_computePreApplyCoalitionCost(
				fgraph,
				witnesses,
				flServ.getFcltOpen()
			);

			context.getService(
				IFcltCoalitionEventsListener.class
			).onCoalitionApply(this, enemies);
			applyCoalition(enemies, flServ);
			context.getService(ICoalitionsRegistry.class)
				.increaseCoalitionPairsVersion();

			assert _ac_allAgsInCoalsAreClosed(flServ, hcagReg);
			assert Math.abs(costPre - profit -
				GraphSupportFront.computeFacilityLocationCost(
					fgraph, flServ.getFcltOpen(), witnesses)
			) < eps;
		}
		return whileService.isAgentAlive(this);
	}

	public void increaseCoalitionVersion() {
		coalitionVersion++;
	}

	public int getCoalitionVersion() {
		return coalitionVersion;
	}

	public int getNegotiationegVersion() {
		return negotiationegVersion;
	}

	public boolean isAlive(IFcltLocDataService flServ) {
		return ownerAgent.getCoalition(flServ) == this;
	}

	private void syncNextVersion(
		IFcltLocDataService flServ,
		ICoalitionsRegistry registry
	) {
		this.solutionVersion = flServ.getSolutionVersion();
		coalitionVersion++;
		negotiationegVersion = coalitionVersion;
		registry.onCoalitionVersionIncreased(this);
	}

	private double computeTryApplyProfit(
		IFcltLocDataService flServ,
		IHillClimbingAgentRegistry hcagReg,
		List<Integer> enemies,
		ICoalitionsListener cltnsListen,
		double[] enemyRate
	) {
		FacilitiesGraph fgraph = flServ.getFgraph();
		int[] witnesses = flServ.getWitnesses();
		boolean[] isFcltOpen = flServ.getFcltOpen();
		double[] facilitiesPrices = fgraph.getFacilitiesPrices();
		int facilitiesCount = fgraph.getFacilitiesCount();

		double profit;
		Arrays.fill(enemyRate, 0);

		excludeOpenFacilities(isFcltOpen); 
		assert _ac_allCoalitionMemsAreClosed(isFcltOpen);

		markTemporaryOpen(this, flServ);
		try {
			double coalitionCost =
				computeCoalitionCost(fgraph, this);

			for (
				int enemyFclt = 0;
				enemyFclt < facilitiesCount;
				enemyFclt++
			) {
				CollaboratingFacilityAgent enemyAgent =
					hcagReg.getCollFcltAgent(enemyFclt);
				if (
					isFcltOpen[enemyFclt] &&
					!members.contains(enemyAgent)
				) {
					enemyRate[enemyFclt] += facilitiesPrices[enemyFclt];
					enemyRate[enemyFclt] -= coalitionCost;
					enemyRate[enemyFclt] += computeRedistributeFacilityProfit(
						fgraph,
						witnesses,
						isFcltOpen,
						enemyFclt
					);
				} else {
					enemyRate[enemyFclt] = -fgraph.getInfEdgeValue();
				}
			}
			normRate(enemyRate, fgraph.getInfEdgeValue());

			computeSortedEnemies(
				enemyRate,
				hcagReg,
				isFcltOpen,
				enemies
			);
			assert _ac_checkEnemiesSorted(enemies, enemyRate);

			cltnsListen.onFindProfitableEnemiesBeg();
			profit = findProfitableEnemies(
				fgraph,
				witnesses,
				isFcltOpen,
				enemies
			);
			cltnsListen.onFindProfitableEnemiesEnd();
		} finally {
			unmarkTemporaryOpen(this, flServ);
		}

		return profit;
	}

	private double findProfitableEnemies(
		FacilitiesGraph fgraph,
		int[] witnesses,
		boolean[] isFcltOpen,
		List<Integer> enemies
	) {
		int enemyCount = enemies.size();
		assert _ac_containsOnlyOpen(enemies, isFcltOpen);
		double[] facilitiesPrices =
			fgraph.getFacilitiesPrices();
		double infEdgeValue = fgraph.getInfEdgeValue();

		double closedEnemiesSum = 0;
		double coalitionCost =
			computeCoalitionCost(fgraph, this);

		try {
			if (useFcltRepaymentHeuristic) {
				double noEnemyProfit = computeBestAllWitnessProfit(
					fgraph,
					witnesses,
					isFcltOpen
				);
				noEnemyProfit -= coalitionCost;
				if (noEnemyProfit > eps &&
					members.size() > fcltRepayCoalLimit
				) {
					enemies.clear();
					return noEnemyProfit;
				}
			}

			for (int i = 0; i < enemyCount; i++) {
				int enemy = enemies.get(i);
				assert isFcltOpen[enemy];
				isFcltOpen[enemy] = false;
				closedEnemiesSum += facilitiesPrices[enemy];
				double redistrClosedProfit =
					computeBestAllWitnessProfit(fgraph, witnesses, isFcltOpen);
				double totalProfit = redistrClosedProfit -
					coalitionCost +
					closedEnemiesSum;

				if (totalProfit > eps || totalProfit < -infEdgeValue) {
					BaseUtils.removeTail(enemies, i + 1);
					return totalProfit;
				}
			}
		} finally {
			for (int enemy : enemies) {
				isFcltOpen[enemy] = true;
			}
		}
		return -1;
	}

	private void computeSortedEnemies(
		final double[] enemyRate,
		IHillClimbingAgentRegistry hcagReg,
		boolean[] isFcltOpen,
		List<Integer> enemies
	) {
		int facilitiesCount = enemyRate.length;
		for (
			int enemyFclt = 0;
			enemyFclt < facilitiesCount;
			enemyFclt++
		) {
			CollaboratingFacilityAgent enemyAgent =
				hcagReg.getCollFcltAgent(enemyFclt);
			if (
				isFcltOpen[enemyFclt] &&
				!members.contains(enemyAgent)
			 ) {
				enemies.add(enemyFclt);
			}
		}
		Collections.sort(
			enemies,
			new Comparator<Integer>() {
				@Override
				public int compare(Integer a, Integer b) {
					double ratea = enemyRate[a];
					double rateb = enemyRate[b];
					return
						ratea > rateb + eps ?
							-1 :
							rateb > ratea + eps ? 1 : 0;
				}
			}
		);
	}

	private void excludeOpenFacilities(
		boolean[] isFcltOpen
	) {
		for (
			Iterator<CollaboratingFacilityAgent> iter =
				members.iterator();
			iter.hasNext();
		) {
			CollaboratingFacilityAgent agent = iter.next();
			if (isFcltOpen[agent.getFacility()]) {
				iter.remove();
			}
		}
	}

	// ### ### ### ### ### ### ### ### ### ### ### ### ###

	private void applyCoalition(
		List<Integer> enemies,
		IFcltLocDataService flServ
	) {
		markTemporaryOpen(this, flServ);
		boolean[] isFcltOpen = flServ.getFcltOpen();
		FacilitiesGraph fgraph = flServ.getFgraph();
		int[] witnesses = flServ.getWitnesses();

		for (int enemy : enemies) {
			isFcltOpen[enemy] = false;
		}
		computeBestAllWitnessProfit(
			fgraph,
			witnesses,
			isFcltOpen,
			true
		);

		unlinkAgents();
	}

	// ### ### ### ### ### ### ### ### ### ### ### ### ###

	private void unlinkAgents() {
		for (CollaboratingFacilityAgent agent : members) {
			agent.setCoalition(null);
		}
	}

	@Override
	public String toString() {
		return "coalition: " + members.toString();
//		return "coalition: " + id + ": " + members.toString();
	}

	// assertion checks

	public boolean _ac_allCoalitionMemsAreClosed(
		boolean[] isFcltOpen
	) {
		for (CollaboratingFacilityAgent agent : members) {
			assert !isFcltOpen[agent.getFacility()] :
				"bad coalition member " + agent;
			if (isFcltOpen[agent.getFacility()]) {
				return false;
			}
		}
		return true;
	}

	private boolean _ac_checkEnemiesSorted(
		List<Integer> enemies,
		double[] enemyRate
	) {
		for (int i = 0; i < enemies.size() - 1; i++) {
			double a = enemyRate[enemies.get(i)];
			double b = enemyRate[enemies.get(i + 1)];
			if (b > a + eps) {
				assert b > a + eps :
					"mall formed enemies list: " +
						enemies + " : " + i;
				return false;
			}
		}
		return true;
	}

	private boolean _ac_containsOnlyOpen(
			List<Integer> enemies,
			boolean[] isFcltOpen
	) {
		for (int enemy : enemies) {
			assert isFcltOpen[enemy] :
				"bad open enemy: " + enemy;
			if (!isFcltOpen[enemy]) {
				return false;
			}
		}
		return true;
	}

	private double _ac_computePreApplyCoalitionCost(
		FacilitiesGraph fgraph,
		int[] witnesses,
		boolean[] isFcltOpen
	) {
		double costPre = BaseUtils.globalAssertionEnabled() ?
			GraphSupportFront.computeFacilityLocationCost(
				fgraph,
				isFcltOpen,
				witnesses
			) :
			Double.NaN;
		return costPre;
	}

	private boolean _ac_allAgsInCoalsAreClosed(
		IFcltLocDataService flServ,
		IHillClimbingAgentRegistry hcagReg
	) {
		boolean[] isFcltOpen = flServ.getFcltOpen();
		for (
			FacilityCoalition coal :
				FcltCityNegotiateComputator.getAllCoalitions(hcagReg, flServ)
		) {
			for (CollaboratingFacilityAgent fag : coal.getMembers()) {
				assert !isFcltOpen[fag.getFacility()];
				if (isFcltOpen[fag.getFacility()]) {
					return false;
				}
			}
		}
		return true;
	}
	
	private boolean _ac_sameRateANDProfitNull(INegotiationContext context) {
		IFcltLocDataService flServ =
			context.getService(IFcltLocDataService.class);
		IHillClimbingAgentRegistry hcagReg =
			context.getService(IHillClimbingAgentRegistry.class);
		ICoalitionsListener cltnsListen =
			context.getService(ICoalitionsListener.class);
		
		List<Integer> enemies = new ArrayList<Integer>();
		int facilitiesCount = flServ.getFgraph().getFacilitiesCount();
		double[] enemyRate = new double[facilitiesCount];
		double theProfit = computeTryApplyProfit(
			flServ,
			hcagReg,
			enemies,
			cltnsListen,
			enemyRate
		);
		assert theProfit < eps;
		if (theProfit > eps) {
			return false;
		}
		
		for (int i = 0; i < facilitiesCount; i++) {
			assert Math.abs(enemyRate[i] - this.enemyRate[i]) < eps;
			if (Math.abs(enemyRate[i] - this.enemyRate[i]) >= eps) {
				return false;
			}
		}
		
		return true;
	}
}
